<?php

/**
 * Albireo Kernel
 *
 * @copyright  Copyright (c) 2010 Albireo Solutions
 * @package    Kernel
 */
namespace Celebrio\AccessControl;

use \Nette\Object;
use \Nette\Environment;
use Nette\Collections\ArrayList;

use dibi;
use DibiException;
use DibiDataSource;

use Celebrio\PasswordHasher;
use Celebrio\Core\ServiceProvider;
use \Celebrio\Modules;
use \Celebrio\Core\NoSuchServiceException;
use Celebrio\AccessControl\AlbireoUser;

use KernelModule\UsersModule\UserException;
use KernelModule\UsersModule\UserSearchingException;

use \Celebrio\FileSystem\FileNotFoundException;

use KernelModule\AclModule\AclRole;

use \ApplicationsModule\PeopleModule\PeopleManagement;
use \ApplicationsModule\PeopleModule\Person;


/**
 * User Management
 *
 * @author Albireo Solutions
 */

class UserManagement extends Object {

    private $roles = "roles";

    public static $creation_hooks = array();

    /**
     * Adds User to Database
     *
     * @throws \KernelModule\UsersModule\UserException
     * @param AlbireoUser $user
     * @param string $password
     * @return bool
     */
    public function addUser(AlbireoUser $user, $password) {

        if (is_null($user->getUsername()) ||
            is_null($user->getLanguage()) ||
            is_null($user->getTimezone())
        ) {
        	\Logger::getRootLogger()->error("something important in user is null");
            throw new UserException("something important is null");
        }
        try {
            dibi::query("INSERT INTO [users]([user_name],[first_name],[last_name],[language],[time_zone],[status]) VALUES
                        (%sN,%sN,%sN,%sN,%sN,%sN)",
                        $user->getUsername(), $user->getFirstName(), $user->getLastName(), $user->getLanguage(), $user->getTimezone(),
                        AlbireoUser::FREE);
            $user->setId(dibi::getInsertId());
            $this->addPassword($user, $password, true);

            // temporary hook to People - ugly
            try {
                $pm = ServiceProvider::getService(Modules::PEOPLE);
                $pm->createPerson($user);
            } catch (NoSuchServiceException $e) {
                // people not installed
            }

            return true;
        } catch (\DibiException $e) {
        	\Logger::getRootLogger()->error("Database problem while creating user", $e);
            throw new UserException("Could not create user", $e->getCode(), $e);
        }
    }

    public function updateUser(AlbireoUser $user, $password = null) {
        try {
            dibi::query("UPDATE [users] SET %a WHERE id = %i",
                        array(
                             'user_name' => $user->getUsername(),
                             'first_name' => $user->getFirstName(),
                             'last_name' => $user->getLastName(),
                             'language' => $user->getLanguage(),
                             'time_zone' => $user->getTimezone(),
                        ),
                        $user->getUserId()
            );
            if (!empty($password)) {
                $this->addPassword($user, $password, false);
            }
            return true;
        } catch (\DibiException $e) {
        	\Logger::getRootLogger()->warn("Database error when updating user");
            return false;
        }
    }

    /**
     * Deletes user. All DB records, files in VFS or related user data is deleted as well.
     *
     * @param AlbireoUser $user
     * @return bool
     */
    public function deleteUser(AlbireoUser $user) {
        try {
            if ($user->getUsername() == null) {
                throw new \Exception("User with no name given");
            }

            // delete user entry
            dibi::query("DELETE FROM [users] WHERE [user_name] = %s", $user->getUsername());

            // delete the person entry. Contact information is deleted via cascade. Also deletes
            // all the persons linked to this user
            $pm = new PeopleManagement();
            $person_rows = dibi::select("id_person")->from("people")->where(array("id_user" => $user->getUserId()))->fetchAll();
            foreach ($person_rows as $person_row) {
                $person = $pm->getPerson($person_row["id_person"]);
                $person->destroy();
            }

            // delete /usr/$username directory
            try {
                $fs = ServiceProvider::getService(Modules::FS);
                $fs->delete("/usr/" . $user->getUsername());
            } catch (FileNotFoundException $e) {
                // /usr/$username directory has not been created yet
            }

            // destroy user's storage container (azure) for FS
            $fs_driver = ServiceProvider::getService(Modules::FS_DRIVERS);
            $fs_driver->cleanUser($user);

            // delete the login from database engine
            ServiceProvider::getService("Celebrio-Database-IDatabase")->deleteLogin($user->getUsername());


            return true;
        } catch (\DibiException $e) {
        	\Logger::getRootLogger()->warn("Database problem while deleting user", $e);
            return false;
        }
    }

    public function deleteAllGuests() {
        $query = dibi::select("users.id, users.user_name")->from("users")->innerJoin("users_roles")
                ->on("[users].[id] = [users_roles].[user_id]")
                ->innerJoin("roles")
                ->on("[roles].[id] = [users_roles].[role_id]")
                ->where(array("roles.name" => AclConstants::GUEST_ROLE));

        foreach ($query->fetchAll() as $row) {
            // $this->getUserByUsername would be better, but more expensive
            $guest_user = new AlbireoUser($row["user_name"]);
            $guest_user->setId($row["id"]);
            $this->deleteUser($guest_user);
        }
    }

    /**
     * ADD or UPDATE pass (by setting $newUser = false) - in Albireo
     * you don't update
     *
     * @param AlbireoUser $user
     * @param string $password
     * @param bool $newUser
     * @return bool
     */
    public function addPassword(AlbireoUser $user, $password, $newUser = false) {
        if ($user->getId() == 0 || empty($password)) {
        	\Logger::getRootLogger()->warn("Password is empty or user id is 0.");
            return false;
        }
        try {
            $pass = PasswordHasher::userHash($user->getUsername(), $password);
            dibi::query("INSERT INTO [passwords]([user_id],[password]) VALUES
                        (%iN,%sN)", $user->getId(), $pass);
            return ServiceProvider::getService("Celebrio-Database-IDatabase")->createLogin($user->getUsername(), $pass, $newUser);
        } catch (DibiException $e) {
        	\Logger::getRootLogger()->warn("Database error when adding password to user.");
            return false;
        }
    }

    public function getUsersDataSource() {
        return new DibiDataSource("users", dibi::getConnection());
    }

    /**
     * returns user from db by his id
     *
     * @param int $user_id
     * @return AlbireoUser
     */
    public function getUser($user_id) {
        try {
            $userArray = dibi::fetch("SELECT [id],[user_name],[first_name],[last_name],[created],[language],[time_zone]
            FROM [users] WHERE [id] = %i", $user_id);
            if ($userArray) {
                $user = new AlbireoUser($userArray["user_name"],
                    $userArray["first_name"],
                    $userArray["last_name"],
                    $userArray["created"],
                    $userArray["language"],
                    $userArray["time_zone"]);
                $user->setId($userArray["id"]);
                return $user;
            } else {
            	// user does not exist + user array
            	\Logger::getRootLogger()->warn("User does not exist.");
                return null;
            }
        } catch (\DibiException $e) {
        	\Logger::getRootLogger()->warn("Database problem while getting user $user_id.", $e);
            return null;
        }
    }

    public function getUsers($users) {
        $fluent = dibi::select("[id], [user_name]")->from("[users]");
        $first = true;
        foreach ($users as $user) {
            if ($first) {
                $first = false;
                $fluent->where("[id] = %i", $user);
            } else {
                $fluent->or("[id] = %i", $user);
            }
        }
        return $fluent->fetchAssoc("id");
    }

    /**
     * Checks given username whether it has already been used by another user or not.
     *
     * The method select from database all users with username similar to the given one.
     * It ignores upper/lower case since we don't allow both 'John.Doe' and 'john.doe' and
     * takes all the users which match the pattern 'usernameXY' where XY is number, for
     * example 'john.doe2'. Then the first "available" username is returned, which means
     * the lowest number (or no number in the best case) which has not been taken yet.
     *
     * @param $username
     * @return string available username to the given username
     */
    public function tryUsername($username) {
        // similar usernames which start with $username
        $similar = dibi::select("user_name")->from("users")
                ->where("[user_name] LIKE %like~", strtolower($username))
                ->orderBy("user_name")->fetchAll();
        $adept = $username;
        $existing = array_map(function($s) {return strtolower($s["user_name"]); }, $similar);


        // search for the first free name
        // is the $adept username already taken?
        while (in_array(strtolower($adept), $existing)) {
            preg_match("/^" . strtolower($username) . "(?P<number>[0-9]*)$/", strtolower($adept), $matches);
            $adept = $username . ($matches["number"] + 1);
        }

        return $adept;
    }

    /**
     * returns user from db by username
     *
     * @param String $username
     * @return AlbireoUser
     */
    public function getUserByUsername($username) {
        try {
            $userArray = dibi::fetch("SELECT [id],[user_name],[first_name],[last_name],[created],[language],[time_zone]
            FROM [users] WHERE [user_name] = %s", $username);
            if ($userArray) {
                $user = new AlbireoUser($userArray["user_name"],
                    $userArray["first_name"],
                    $userArray["last_name"],
                    $userArray["created"],
                    $userArray["language"],
                    $userArray["time_zone"]);
                $user->setId($userArray["id"]);
                return $user;
            } else {
            	\Logger::getRootLogger()->warn("User does not exist.");
                return null;
            }
        } catch (\DibiException $e) {
        	\Logger::getRootLogger()->warn("Database problem while getting user by name: $username", $e);
            return null;
        }
    }

    /**
     * Connects user in DB with existing Role in DB
     *
     * @param AlbireoUser $user
     * @param AclRole $role
     * @return bool
     */
    public function addUserToRole(AlbireoUser $user, AclRole $role) {
        if ($user->getId() == 0 || $role->getId() == 0) {
            return false;
        }
        try {
            dibi::query("INSERT INTO [users_roles] ([user_id],[role_id]) VALUES (%iN,%iN);", $user->getId(), $role->getId());
        } catch (\DibiException $e) {
        	\Logger::getRootLogger()->warn("Database problem while adding user to role", $e);
            return false;
        }
    }

    /**
     * "Smart search" method. Tries to find any user with the username, first name,
     * or last name similar to the $input parameter. When the input
     * contains space, only first name and surname is searched (but in both orders).
     *
     * @param String $input the string we want to "smart search", for example part of the e-mail address
     * @param int $limit The maximum users we want to have in result (0 = unlimited)
     * @return DataSource containing the users who match input, ordered by UserName
     */
    public function getFilteredUsersDataSource($input, $limit = 0) {
        $spaces = strstr($input, " ");
        if ($spaces === FALSE) {
            $input = $input . "%";
            $ds = $this->getUsersDataSource()->where("%or", array(
                 array("[user_name] LIKE %s", $input),
                 array("[first_name] LIKE %s", $input),
                 array("[last_name] LIKE %s", $input),
            ));

            //\Nette\Debug::dump($this->getUsersDataSource()->where("[user_name] < %s", $input)->count());
        } else if (substr_count($input, " ") == 1) {
            $names = explode(" ", $input);
            $ds = dibi::getConnection()->dataSource("SELECT * FROM [users] WHERE
                    ([first_name] LIKE %s AND [last_name] LIKE %s) OR
                    ([first_name] LIKE %s AND [last_name] LIKE %s)",
                    $names[0] . "%", $names[1] . "%", $names[1] . "%", $names[0] . "%");
        } else {
            throw new UserSearchingException("more than 2 spaces");
        }
        $ds->orderBy("user_name");

        if ($limit != 0) {
            $ds->applyLimit($limit);
        }
        
        return $ds;
    }
    
    /**
     * This method is similar to previous but takes only last name and
     * first name. Is used near retrieving permissions from facebook of
     * user which wants to share data with grandma who has Celebrio.
     * Should be refactored together the previous method
     * @param string $input as previous
     * @param type $limit - || -
     * @return type 
     */
    public function getFilteredUsersDataSourceOnlyName($input, $limit = 0) {
        $spaces = strstr($input, " ");
        if ($spaces === FALSE) {
            $input = $input . "%";
            $ds = $this->getUsersDataSource()->where("%or", array(
                                                                 array("[first_name] LIKE %s", $input),
                                                                 array("[last_name] LIKE %s", $input),
                                                            ));

            //\Nette\Debug::dump($this->getUsersDataSource()->where("[user_name] < %s", $input)->count());
        } else if (substr_count($input, " ") == 1) {
            $names = explode(" ", $input);
            $ds = dibi::getConnection()->dataSource("SELECT * FROM [users] WHERE
                ([first_name] LIKE %s AND [last_name] LIKE %s) OR
                ([first_name] LIKE %s AND [last_name] LIKE %s)",
                                                    $names[0] . "%", $names[1] . "%", $names[1] . "%", $names[0] . "%"
            );
        } else {
            throw new UserSearchingException("more than 2 spaces");
        }
        $ds->orderBy("user_name");

        if ($limit != 0) {
            $ds->applyLimit($limit);
        }
        
        return $ds;
    }

    public function addRolesForUser($roles, $userId) {
        foreach ($roles as $role) {
            dibi::query('INSERT INTO [users_roles] ([user_id], [role_id]) VALUES (%i, %i);', $userId, $role);
        }
    }

    public function deleteAllRolesForUser($userId) {
        dibi::query('DELETE FROM [users_roles] WHERE [user_id]=%i;', $userId);
    }

    public function getRolesByUserId($userId) {
        $rolesList = new ArrayList();
        try {
            $allRoles = dibi::fetchAll("SELECT * FROM [users_roles] JOIN [roles]
                ON [role_id] = [id]
                WHERE [user_id] = %i", $userId);
        } catch (DibiException $e) {
            return $rolesList;
        }
        foreach ($allRoles as $roleRow) {
            $role = new AclRole();
            $role->setId($roleRow["role_id"]);
            $role->setName($roleRow["name"]);
            $role->setParentId($roleRow["parent_id"]);
            $role->setCreated($roleRow["created"]);
            $rolesList->append($role);
        }
        return $rolesList;
    }

    public function getUsersForRole($roleId) {
        $usersList = new ArrayList();
        try {
            $allUsers = dibi::fetchAll("SELECT * FROM [users_roles] JOIN [users]
                ON [user_id] = [id]
                WHERE [role_id] = %i
                ORDER BY user_name", $roleId);
        } catch (DibiException $e) {
            return $usersList;
        }
        foreach ($allUsers as $userRow) {
            $user = new AlbireoUser($userRow["user_name"],
                $userRow["first_name"],
                $userRow["last_name"],
                $userRow["created"],
                $userRow["language"],
                $userRow["time_zone"]);
            $user->setId($userRow["user_id"]);
            $usersList->append($user);
        }
        return $usersList;
    }

    public function updateRole(AclRole $role) {
        try {
            dibi::query("UPDATE [" . $this->roles . "] SET %a WHERE id = %i",
                        array(
                             "name" => $role->getName(),
                             "parent_id" => $role->getParentId(),
                        ),
                        $role->getId()
            );
            return true;
        } catch (DibiException $e) {
        	\Logger::getRootLogger()->warn("Database problem while updating role", $e);
            return false;
        }
    }

    public function getRoleById($id) {
        try {
            $roleArray = dibi::fetch("SELECT [id],[name],[parent_id],[created] FROM [" . $this->roles . "] WHERE [id] = %i", $id);
            if ($roleArray) {
                $role = new AclRole();
                $role->setId($roleArray["id"]);
                $role->setParentId($roleArray["parent_id"]);
                $role->setName($roleArray["name"]);
                $role->setCreated($roleArray["created"]);
                return $role;
            } else {
                return null;
            }
        } catch (DibiException $e) {
        	\Logger::getRootLogger()->warn("Database problem while getting role by id.", $e);
            return null;
        }
    }
}
